Skip to content

fix(core): return error instead of panicking on integer division by zero 💥#168

Merged
timfennis merged 2 commits into
masterfrom
claude/festive-bardeen-ZVz6p
Jun 2, 2026
Merged

fix(core): return error instead of panicking on integer division by zero 💥#168
timfennis merged 2 commits into
masterfrom
claude/festive-bardeen-ZVz6p

Conversation

@timfennis
Copy link
Copy Markdown
Owner

Summary

Integer %, %% and \ by zero crashed the interpreter with a Rust panic (attempt to divide by zero) originating in i64/num-bigint arithmetic, aborting the whole process instead of producing a recoverable runtime error.

The simplest reproducer:

1 % 0
thread 'main' panicked at num-bigint-0.4.6/.../division.rs:112:9:
attempt to divide by zero

The same panic was reachable through several operators / operand types:

Input Before After
5 % 0 (int modulo) panic error[vm]: division by zero
5 \ 0 (floor div) panic error[vm]: division by zero
bigint % 0 panic error[vm]: division by zero
(1/3) % 0 (rational) panic error[vm]: division by zero
0 % 0 panic error[vm]: division by zero
5 / 0 inf inf (unchanged)
5.0 % 0.0 NaN NaN (unchanged)

Changes

  • ndc_core/src/num.rs — hand-written Number: Rem impl that rejects exact (int/rational) remainder by zero up front; same guard added to floor_div and checked_rem_euclid. Added a Number::is_zero helper.
  • ndc_stdlib/src/math.rs — replaced the macro-generated integer % operator (whose fast-path fallback panicked) with one that checks for a zero divisor explicitly while preserving the i64::MIN % -1 overflow fallback.
  • Regression tests — 5 new .ndc functional tests under tests/functional/programs/001_math/ (auto-discovered by build.rs).
  • Manual — documented division-by-zero semantics in number.md.

/ keeps returning infinity and float % keeps returning NaN, matching the existing documented semantics — only the exact-arithmetic integer/rational paths that previously panicked now error cleanly.

Verification

  • cargo fmt clean
  • cargo clippy — no new warnings (28 pre-existing in both baseline and current)
  • cargo test — all pass, including the 5 new regression tests

https://claude.ai/code/session_01RaWdHLsFAeQCf423gGpt3h


Generated by Claude Code

Comment thread tests/functional/programs/001_math/028_rational_modulo_by_zero.ndc
claude added 2 commits June 2, 2026 07:19
…ero 💥

Integer `%`, `%%` and `\` by zero crashed the interpreter with a Rust
panic ("attempt to divide by zero") originating in i64/num-bigint
arithmetic. `1 % 0`, `5 \ 0`, a BigInt `% 0` and a rational `% 0` all
aborted the process instead of producing a recoverable runtime error.

Guard the exact-arithmetic remainder and floor-division paths against a
zero divisor and surface a "division by zero" error. Float `%` keeps its
IEEE NaN behaviour and `/` keeps returning infinity, matching the
existing documented semantics.

Adds functional regression tests and documents the behaviour in the
manual.
The previous commit over-corrected: it made floor division (`\`) by zero
raise an error for every operand type. But only the i64 fast path actually
panicked — rational and BigInt floor division by zero already promoted to a
float and returned infinity, matching `/`. Erroring there was a regression.

Guard just the i64 `div_euclid` fast path (which also panics on
`i64::MIN \ -1`) and fall back to the general float-promoting path, so all
floor divisions by zero return `inf` again. Remainder (`%`, `%%`) still
errors, since those genuinely panicked before and have no infinite result.

Updates the floor-div regression tests and the manual accordingly.
@timfennis timfennis force-pushed the claude/festive-bardeen-ZVz6p branch from bed32e7 to 272c7fc Compare June 2, 2026 05:19
@timfennis timfennis enabled auto-merge (squash) June 2, 2026 05:19
@timfennis timfennis merged commit d83a480 into master Jun 2, 2026
1 check passed
@timfennis timfennis deleted the claude/festive-bardeen-ZVz6p branch June 2, 2026 05:20
timfennis added a commit that referenced this pull request Jun 2, 2026
…ases 💥 (#171)

## Context

While fuzzing the pipeline for panics, I found that the integer-power
(`^`) path aborts the whole process on several negative-exponent inputs.
These are recoverable runtime errors that should never crash the
interpreter — same class of bug as the recent integer division-by-zero
fix (#168).

## The crashes

| Input | Panic |
|---|---|
| `2 ^ (1/-1)` | `right hand side must not be negative` (`int.rs:117`) |
| `0 ^ -1` | `denominator == 0` (num-rational) |
| `0 ^ (2/-1)` | `right hand side must not be negative` |

**Root cause** — `Number::pow` had two integer-exponent branches:
- `Int ^ Int` handled negative exponents as the reciprocal
`1/(base^|exp|)`, but with base `0` that builds `BigRational::new(1,
0)`, which panics.
- `Int ^ Rational` (integer-valued rational, e.g. `1/-1`) didn't handle
negatives at all — it called `Int::pow` directly, which panics
unconditionally on a negative RHS.

The existing `MAX_EXPONENT_BITS` guard only checks magnitude, so it
never caught these.

## Changes

- Added a `Number::int_pow(base, exponent)` helper that both
integer-exponent branches route through. It returns a `division by zero`
error for `0 ^ negative`, computes the reciprocal rational for any other
negative exponent, and otherwise returns the plain integer power. This
also removes the previously duplicated negative-exponent logic.
- Regression tests:
- `900_bugs/bug0025_pow_negative_rational_exponent.ndc` — `2 ^ (1/-1)` →
`1/2`
- `001_math/030_pow_zero_negative_exponent.ndc` — `0 ^ -1` → `division
by zero`
- `001_math/031_pow_zero_negative_rational_exponent.ndc` — `0 ^ (2/-1)`
→ `division by zero`

Valid cases verified unchanged: `2 ^ -1` → `1/2`, `3 ^ (-4/2)` → `1/9`,
`2 ^ 3` → `8`, `(1/2) ^ -3` → `8`.

🤖

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants